/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package com.googlecode.datawander.codecompiler;

import com.googlecode.datawander.i18n.I18n;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import org.apache.log4j.Logger;

/**
 *
 * @author Marcin Stachniuk
 */
public class CodeCompiler {

    private static final Logger logger = Logger.getLogger(CodeCompiler.class);

    /**
     * Field that have a path to the root of source code. This field is used in
     * compileOneFile method, to add his value do classpath in java command.
     * Field mainSourcePathName can't be File.separator ended.
     */
    private String mainSourcePathName;

    /**
     * Test is Java Copiler (javac) instal, and is possible ro run this command.
     * @throws CompileCodeException If is impossible to run javac cmopiler.
     * Propably environment variable PATH is not set do javac.
     */
    public static void isCompilerInstal() throws CompileCodeException {
        logger.trace("isCompilerInstal()");

        Process process;
        try {
            process = Runtime.getRuntime().exec("javac -version");
            InputStream is = process.getErrorStream();
            BufferedReader in = new BufferedReader(new InputStreamReader(is));
            String line = in.readLine();

            String readline = I18n.getString("readline");
            readline = String.format(readline, line);
            logger.trace(readline);
            logger.info(I18n.getString("compilationTestSuccess"));
        } catch (IOException ex) {
            logger.error(ex, ex);
            throw new CompileCodeException(I18n.getString("nojavaccompiler"));
        }
    }

    /**
     * Comopile Java all source code in sourcePathName directory.
     * @param directory Path to folder with source code.
     * @param outputPathName Path to folder to output bytecode.
     * @throws CompileCodeException If is compile time error.
     */
    public void compileSourceCode(String sourcePathName, String outputPathName) throws CompileCodeException {
        logger.trace("compileSourceCode("+sourcePathName+", "+outputPathName+")");

        File source = new File(sourcePathName);
        if(!source.exists()) {
            String srcFolderNotExist = I18n.getString("sourcefoldernotexist");
            srcFolderNotExist = String.format(srcFolderNotExist, sourcePathName);
            throw new CompileCodeException(srcFolderNotExist);
        }

        File output = new File(outputPathName);
        //TODO: test czy dziala, kompilator powinien tworzyć katalog docelowy o ile on nie istnieje
        output.mkdirs();

        if(!output.isDirectory()) {
            String isntDirectiory = I18n.getString("outputFolderIsNotAFolder");
            isntDirectiory = String.format(isntDirectiory, output.getAbsolutePath());
            throw new CompileCodeException(isntDirectiory);
        }

        cleanDirectory(outputPathName);

        logger.debug(I18n.getString("startcompilationprocess"));

        // set main source path (needed in generate command to compile a file)
        // main source path can't be File.separator ended.
        this.mainSourcePathName = 
                (sourcePathName.endsWith(File.separator) ?
                    sourcePathName.substring(0, sourcePathName.length() - 1) :
                    sourcePathName);

        boolean isSomethigCompile = compileSourceFolderLoop(sourcePathName, outputPathName);

        if(!isSomethigCompile) {
            throw new CompileCodeException(I18n.getString("nofilestocompile"));
        }

        logger.debug(I18n.getString("endcompilationprcess"));
    }

    /**
     * Compile whole source folder, all *.java files. This is recurnency loop
     * and don't invore this method directly. Only compileSourceCode and they
     * self can invole this method.
     * @param sourcePathName Source code directiory.
     * @param outputPathName Output byte code directory.
     * @return Return true, if one or more file was compiled.
     * @throws CompileCodeException If it is compile time error.
     */
    private boolean compileSourceFolderLoop(String sourcePathName, String outputPathName) throws CompileCodeException {
        File file = new File(sourcePathName);
        File [] files = file.listFiles(new JavaFileFilter());

        boolean isSomethigCompile = false;

        //TODO: Obsługa katalogów
        for(File f : files) {
            if(f.isDirectory()) {
                isSomethigCompile = compileSourceFolderLoop(f.getAbsolutePath(),
                        outputPathName);
            } else {
                compileOneFile(sourcePathName, outputPathName, f.getName());
                isSomethigCompile = true;
            }
        }

        return isSomethigCompile;
    }

    /**
     * Try compile one source file.
     * @param sourcePath Path to folder with source code.
     * @param outputPathName Path to folder to output bytecode.
     * @param fileName name of file to compile.
     * @throws CompileCodeException If it is compile time error.
     */
    private void compileOneFile(String sourcePath, String outputPathName, String fileName) throws CompileCodeException {
        logger.trace("compileOneFile("+sourcePath+", "+outputPathName+", "+fileName+")");

        String pathSeparator = System.getProperty("path.separator");

        outputPathName = toDirectoryWithoutLastSeperatorChar(outputPathName);
        sourcePath = toDirectoryWithoutLastSeperatorChar(sourcePath);

        logger.trace("mainSourcePathName " + mainSourcePathName + " sourcePath: " + sourcePath);

        Process process;
        String command = "javac -d \"" + outputPathName + "\" -cp \"%CLASSPATH%" + pathSeparator
                + generatePathToOracleJar() + pathSeparator + generatePathToDb4oJar() + pathSeparator +
                mainSourcePathName + "\" ";
                //sourcePath + "\" ";

        // absolute path to source file
        command = command + "\"" + sourcePath + File.separator + fileName + "\"";

        String commandMessage = I18n.getString("command");
        commandMessage = String.format(commandMessage, command);
        logger.debug(commandMessage);
        try {

            process = Runtime.getRuntime().exec(command);
            InputStream is = process.getErrorStream();
            BufferedReader in = new BufferedReader(new InputStreamReader(is));
            String line = in.readLine();
            if(line == null) {
                String sucessMessage = I18n.getString("compilationsucessoffile");
                sucessMessage = String.format(sucessMessage, fileName);
                logger.info(sucessMessage);
            } else {
                generateJavaCompilerException(line);
            }
        } catch (IOException ex) {
            logger.error(ex, ex);
            throw new CompileCodeException(I18n.getString("nojavaccompiler"));
        }
    }

    /**
     * Generate new CompileCodeException with description from line.
     * @param line error description.
     * @throws CompileCodeException Return always, when method is called.
     */
    private void generateJavaCompilerException(String line) throws CompileCodeException {
        String message = I18n.getString("compilatorreturnedline");
        message = String.format(message, line);
        logger.info(message);
        throw new CompileCodeException(line);
    }

    /**
     * Delete contents of directory.
     * @param directory Directory to clean
     */
    public void cleanDirectory(String directory) {
        logger.trace("cleanDirectory("+directory+")");

        String cleanMessage = I18n.getString("cleandirectory");
        cleanMessage = String.format(cleanMessage, directory);
        logger.info(cleanMessage);

        deleteDir(new File(directory));
    }

    /**
     * Delete cascade directory. Method delete non-empty directory's
     * @param dir
     */
    private void deleteDir(File dir) {
        logger.trace("deleteDir("+dir.getAbsolutePath()+")");
        File [] files = dir.listFiles();

        for(File f : files) {
            if(f.isDirectory()) {
                deleteDir(f);
                f.delete();
            } else {
                logger.trace("delete file: " + f.getName() + " " + f.delete());
            }
        }
    }

    private String generatePathToOracleJar() {
        logger.trace("generatePathToOracleJar()");
        String userDir = System.getProperty("user.dir");

        String result = (userDir.endsWith(""
                + File.separatorChar) ? userDir : userDir+File.separatorChar)
                + "lib"+File.separatorChar+"ojdbc14.jar";

        String generatePathMessage = I18n.getString("generatepathtooraclejar");
        generatePathMessage = String.format(generatePathMessage, result);
        logger.trace(generatePathMessage);
        return result;
    }

    private String generatePathToDb4oJar() {
        logger.trace("generatePathToDb4oJar()");
        String userDir = System.getProperty("user.dir");

        String result = (userDir.endsWith(""
                + File.separatorChar) ? userDir : userDir + File.separatorChar)
                + "lib" + File.separatorChar +
                //"db4o-7.12.132.14217-core-java5.jar";   // 7.12
                "db4o-7.4.106.13438-java5.jar";       // 7.4

        String generatePathMessage = I18n.getString("generatepathtodb4ojar");
        generatePathMessage = String.format(generatePathMessage, result);
        logger.trace(generatePathMessage);
        return result;

    }

    /**
     * Generate string without last separator char (if any). If path not contain
     * separator char at last position is returned path.
     * This method is useful if we need directory path without last separator
     * char.
     * @param path Path to cut.
     * @return
     */
    private String toDirectoryWithoutLastSeperatorChar(String path) {
        if(path.endsWith(File.separator)) {
            return path.substring(0, path.length() - 1);
        }
        return path;
    }
}
